import * as fs from 'fs';
import * as net from 'net';
import * as is from 'is-type-of';
import protocol from './protocol';
import * as helpers from './helpers';
import { Logger } from '@nestjs/common';

export class Storage {
	config: any;
	_name: string;

	readonly logger: Logger = new Logger(Storage.name);

	constructor(config: any) {
		this.config = config;
		this._name = `${config.host}:${config.port}`;
	}

	// methods
	_getConnection(): Promise<net.Socket> {
		return this._newConnection();
	}

	_newConnection(): Promise<net.Socket> {
		const _self = this;
		return new Promise((resolve, reject) => {
			const socket = new net.Socket();
			this.logger.debug('connect to storage server [%s].', _self._name);
			socket.setTimeout(_self.config.timeout);
			socket.connect(_self.config.port, _self.config.host);

			socket.on('error', (err) => {
				reject(err);
			});

			socket.on('timeout', () => {
				socket.destroy();
				reject(new Error(`connect to storage server [${_self._name}] timeout.`));
			});

			socket.on('connect', () => {
				this.logger.debug('storage server [%s] is connected', _self._name);
				resolve(socket);
			});
		});
	}

	/**
	 * # request body:
	 * @ 1 byte: store path index on the storage server
	 * @ FDFS_PROTO_PKG_LEN_SIZE bytes: file size
	 * @ FDFS_FILE_EXT_NAME_MAX_LEN bytes: file ext name, do not include dot (.)
	 * @ file size bytes: file content
	 *
	 * # response body:
	 * @ FDFS_GROUP_NAME_MAX_LEN bytes: group name
	 * @ filename bytes: filename
	 * @param command
	 * @param file
	 * @param options
	 */
	_uploadFile(command: number, file: string | Buffer, options: { size: number; ext: string }): Promise<string> {
		return this._getConnection().then((socket) => {
			this.logger.debug('start upload file to storage server [%s]', this._name);
			// 封装header并发送
			const bodyLength = 1 + protocol.FDFS_PROTO_PKG_LEN_SIZE + protocol.FDFS_FILE_EXT_NAME_MAX_LEN + options.size;
			const header = protocol.packHeader(command, bodyLength, 0);
			socket.write(header);

			// 封装并发送body
			// 除file content外的内容
			const buffer: any = Buffer.alloc(1 + protocol.FDFS_PROTO_PKG_LEN_SIZE + protocol.FDFS_FILE_EXT_NAME_MAX_LEN);
			buffer.writeUInt8(this.config.storePathIndex, 0);
			helpers.number2Buffer(options.size, protocol.FDFS_PROTO_PKG_LEN_SIZE).copy(buffer, 1);
			const extBL = Buffer.byteLength(options.ext, this.config.charset);
			buffer.write(options.ext, 1 + protocol.FDFS_PROTO_PKG_LEN_SIZE, extBL, this.config.charset);

			socket.write(buffer);

			// 发送file content
			if (is.string(file)) {
				file = fs.createReadStream(file) as any;
			}

			// buffer
			if (is.buffer(file)) {
				socket.write(file as any);
			} else {
				(file as any).pipe(socket, { end: false });
			}

			return new Promise<string>((resolve, reject) => {
				protocol.recvPacket(socket, protocol.STORAGE_PROTO_CMD_RESP, null, (err, body) => {
					if (err) {
						reject(err);
						return;
					}
					// 校验body
					if (body.length <= protocol.FDFS_GROUP_NAME_MAX_LEN) {
						throw new Error('response body length: ' + body.length + ' <= ' + protocol.FDFS_GROUP_NAME_MAX_LEN);
					}

					const fileId = _parseFileId(body, this.config.charset);
					resolve(fileId);
				});
			});
		});
	}

	/**
	 * # request body:
	 * @ FDFS_PROTO_PKG_LEN_SIZE bytes: filename length
	 * @ FDFS_PROTO_PKG_LEN_SIZE bytes: file size
	 * @ filename bytes: filename
	 * @ file size bytes: file content
	 *
	 * # response body: none
	 * @param file
	 * @param options
	 */
	_appendFile(file: any, options: { fileId: string; size: number }): Promise<void> {
		return this._getConnection().then((socket) => {
			this.logger.debug('start upload file to storage server [%s]', this._name);
			const gf = helpers.id2gf(options.fileId);
			// 封装header并发送
			const filenameBL = Buffer.byteLength(gf.filename, this.config.charset);
			const bodyLength = 2 * protocol.FDFS_PROTO_PKG_LEN_SIZE + filenameBL + options.size;
			const command = protocol.STORAGE_PROTO_CMD_APPEND_FILE;
			const header = protocol.packHeader(command, bodyLength, 0);
			socket.write(header);

			// 封装并发送body
			// 除file content外的内容
			const buffer: any = Buffer.alloc(2 * protocol.FDFS_PROTO_PKG_LEN_SIZE + filenameBL);
			buffer.fill(0);
			helpers.number2Buffer(gf.filename.length, protocol.FDFS_PROTO_PKG_LEN_SIZE).copy(buffer, 0);
			helpers.number2Buffer(options.size, protocol.FDFS_PROTO_PKG_LEN_SIZE).copy(buffer, protocol.FDFS_PROTO_PKG_LEN_SIZE);
			buffer.write(gf.filename, 2 * protocol.FDFS_PROTO_PKG_LEN_SIZE, filenameBL, this.config.charset);

			socket.write(buffer);

			// 发送file content
			if (is.string(file)) {
				file = fs.createReadStream(file);
			}

			// buffer
			if (is.buffer(file)) {
				socket.write(file as any);
			} else {
				file.pipe(socket, { end: false });
			}

			return new Promise<void>((resolve, reject) => {
				protocol.recvPacket(socket, protocol.STORAGE_PROTO_CMD_RESP, 0, (err) => {
					if (err) {
						reject(err);
						return;
					}
					resolve();
				});
			});
		});
	}

	/**
	 * # request body:
	 * @ FDFS_PROTO_PKG_LEN_SIZE bytes: filename length
	 * @ FDFS_PROTO_PKG_LEN_SIZE bytes: file offset
	 * @ FDFS_PROTO_PKG_LEN_SIZE bytes: file size
	 * @ filename bytes: filename
	 * @ file size bytes: file content
	 *
	 * # response body: none
	 * @param file
	 * @param options
	 */
	_modifyFile(file: any, options: { fileId: string; offset: number; size: number }): Promise<void> {
		return this._getConnection().then((socket) => {
			this.logger.debug('start upload file to storage server [%s]', this._name);
			const gf = helpers.id2gf(options.fileId);
			// 封装header并发送
			const filenameBL = Buffer.byteLength(gf.filename, this.config.charset);
			const bodyLength = 3 * protocol.FDFS_PROTO_PKG_LEN_SIZE + filenameBL + options.size;
			const command = protocol.STORAGE_PROTO_CMD_MODIFY_FILE;
			const header = protocol.packHeader(command, bodyLength, 0);
			socket.write(header);

			// 封装并发送body
			// 除file content外的内容
			const buffer: any = Buffer.alloc(3 * protocol.FDFS_PROTO_PKG_LEN_SIZE + filenameBL);
			buffer.fill(0);
			helpers.number2Buffer(gf.filename.length, protocol.FDFS_PROTO_PKG_LEN_SIZE).copy(buffer, 0);
			helpers.number2Buffer(options.offset, protocol.FDFS_PROTO_PKG_LEN_SIZE).copy(buffer, protocol.FDFS_PROTO_PKG_LEN_SIZE);
			helpers.number2Buffer(options.size, protocol.FDFS_PROTO_PKG_LEN_SIZE).copy(buffer, 2 * protocol.FDFS_PROTO_PKG_LEN_SIZE);
			buffer.write(gf.filename, 3 * protocol.FDFS_PROTO_PKG_LEN_SIZE, filenameBL, this.config.charset);

			socket.write(buffer);

			// 发送file content
			if (is.string(file)) {
				file = fs.createReadStream(file);
			}

			// buffer
			if (is.buffer(file)) {
				socket.write(file as any);
			} else {
				file.pipe(socket, { end: false });
			}

			return new Promise<void>((resolve, reject) => {
				protocol.recvPacket(socket, protocol.STORAGE_PROTO_CMD_RESP, 0, (err) => {
					if (err) {
						reject(err);
						return;
					}
					resolve();
				});
			});
		});
	}

	// upload相关

	// public methods

	/**
	 * # request body:
	 * @ 1 byte: store path index on the storage server
	 * @ FDFS_PROTO_PKG_LEN_SIZE bytes: file size
	 * @ FDFS_FILE_EXT_NAME_MAX_LEN bytes: file ext name, do not include dot (.)
	 * @ file size bytes: file content
	 *
	 * # response body:
	 * @ FDFS_GROUP_NAME_MAX_LEN bytes: group name
	 * @ filename bytes: filename
	 * @param file
	 * @param options
	 */
	public upload(file: string | Buffer, options: { method?: string; size: number; ext: string; fileId?: string; offset?: number }): Promise<string | void> {
		if (!options.method || options.method === protocol.FDFS_METHOD_UPLOAD_FILE) {
			const command = protocol.STORAGE_PROTO_CMD_UPLOAD_FILE;
			return this._uploadFile(command, file, options);
		} else if (options.method === protocol.FDFS_METHOD_UPLOAD_APPENDER_FILE) {
			const command = protocol.STORAGE_PROTO_CMD_UPLOAD_APPENDER_FILE;
			return this._uploadFile(command, file, options);
		} else if (options.method === protocol.FDFS_METHOD_APPEND_FILE) {
			return this._appendFile(file, options as any);
		} else if (options.method === protocol.FDFS_METHOD_MODIFY_FILE) {
			return this._modifyFile(file, options as any);
		}
		return Promise.reject(new Error("options.method must be one of ['upload', 'uploadAppender', 'append', 'modify']"));
	}

	/**
	 * * STORAGE_PROTO_CMD_SET_METADATA
	 *
	 * # function: set meta data
	 * # request body:
	 * @ FDFS_PROTO_PKG_LEN_SIZE bytes: filename length
	 * @ FDFS_PROTO_PKG_LEN_SIZE bytes: meta data size
	 * @ 1 bytes: operation flag,
	 * 'O' for overwrite all old metadata
	 * 'M' for merge, insert when the meta item not exist, otherwise update it
	 * @ FDFS_GROUP_NAME_MAX_LEN bytes: group name
	 * @ filename bytes: filename
	 * @ meta data bytes: each meta data separated by \x01,
	 * name and value separated by \x02
	 * # response body: none
	 * @param fileId
	 * @param metaData
	 */
	public setMetaData(fileId: string, metaData: any, flag?: string): Promise<void> {
		flag = flag || protocol.STORAGE_SET_METADATA_FLAG_OVERWRITE;

		return this._getConnection().then((socket) => {
			const gf = helpers.id2gf(fileId);
			const packedMeta = protocol.packMetaData(metaData);

			// 封装header
			const charset = this.config.charset;
			const command = protocol.STORAGE_PROTO_CMD_SET_METADATA;
			const fnLength = Buffer.byteLength(gf.filename, charset);
			const metaLength = Buffer.byteLength(packedMeta, charset);
			const bodyLength = protocol.FDFS_PROTO_PKG_LEN_SIZE + protocol.FDFS_PROTO_PKG_LEN_SIZE + 1 +
				protocol.FDFS_GROUP_NAME_MAX_LEN + fnLength + metaLength;

			const header = protocol.packHeader(command, bodyLength, 0);
			socket.write(header);

			// 封装body
			const groupLength = Buffer.byteLength(gf.group, charset);
			const body: any = Buffer.alloc(bodyLength);
			body.fill(0);
			helpers.number2Buffer(fnLength, protocol.FDFS_PROTO_PKG_LEN_SIZE).copy(body, 0);
			helpers.number2Buffer(metaLength, protocol.FDFS_PROTO_PKG_LEN_SIZE).copy(body, protocol.FDFS_PROTO_PKG_LEN_SIZE);
			body.write(flag, protocol.FDFS_PROTO_PKG_LEN_SIZE + protocol.FDFS_PROTO_PKG_LEN_SIZE, 1, charset);
			body.write(gf.group, protocol.FDFS_PROTO_PKG_LEN_SIZE + protocol.FDFS_PROTO_PKG_LEN_SIZE + 1, groupLength, charset);
			body.write(gf.filename, protocol.FDFS_PROTO_PKG_LEN_SIZE + protocol.FDFS_PROTO_PKG_LEN_SIZE + 1 + protocol.FDFS_GROUP_NAME_MAX_LEN, fnLength, charset);
			body.write(packedMeta, bodyLength - metaLength, metaLength, charset);

			socket.write(body);

			return new Promise<void>((resolve, reject) => {
				protocol.recvPacket(socket, protocol.STORAGE_PROTO_CMD_RESP, 0, (err) => {
					if (err) {
						reject(err);
						return;
					}
					resolve();
				});
			});
		});
	}

	/**
	 * * STORAGE_PROTO_CMD_GET_METADATA
	 * # function: get meta data from storage server
	 * # request body:
	 * @ FDFS_GROUP_NAME_MAX_LEN bytes: group name
	 * @ filename bytes: filename
	 * # response body
	 * @ meta data buff, each meta data separated by \x01, name and value separated by \x02
	 * @param fileId
	 */
	public getMetaData(fileId: string): Promise<any> {
		return this._getConnection().then((socket) => {
			const gf = helpers.id2gf(fileId);
			const packet = protocol.packFileId(protocol.STORAGE_PROTO_CMD_GET_METADATA, gf.group, gf.filename, this.config.charset);
			socket.write(packet);

			return new Promise<any>((resolve, reject) => {
				protocol.recvPacket(socket, protocol.STORAGE_PROTO_CMD_RESP, null, (err, body) => {
					if (err) {
						reject(err);
						return;
					}
					const rawMeta = body.toString(this.config.charset);
					if (rawMeta) {
						const metaData = protocol.parseMetaData(rawMeta);
						resolve(metaData);
					} else {
						resolve(rawMeta);
					}
				});
			});
		});
	}

	/**
	 * 删除文件
	 * STORAGE_PROTO_CMD_DELETE_FILE
	 * # request body:
	 * @ FDFS_GROUP_NAME_MAX_LEN bytes: group name
	 * @ filename bytes: filename
	 * # response body: none
	 * @param fileId
	 */
	public del(fileId: string): Promise<void> {
		return this._getConnection().then((socket) => {
			const gf = helpers.id2gf(fileId);
			const packet = protocol.packFileId(protocol.STORAGE_PROTO_CMD_DELETE_FILE, gf.group, gf.filename, this.config.charset);
			socket.write(packet);

			return new Promise<void>((resolve, reject) => {
				protocol.recvPacket(socket, protocol.STORAGE_PROTO_CMD_RESP, 0, (err) => {
					if (err) {
						reject(err);
						return;
					}
					resolve();
				});
			});
		});
	}

	/**
	 * STORAGE_PROTO_CMD_DOWNLOAD_FILE
	 * # function: download/fetch file from storage server
	 * # request body:
	 * @ FDFS_PROTO_PKG_LEN_SIZE bytes: file offset
	 * @ FDFS_PROTO_PKG_LEN_SIZE bytes: download file bytes
	 * @ FDFS_GROUP_NAME_MAX_LEN bytes: group name
	 * @ filename bytes: filename
	 * # response body:
	 * @ file content
	 * @param fileId
	 * @param options
	 * options.target 下载的文件流将被写入到这里，可以是本地文件名，也可以是WritableStream
	 * options.offset和options.bytes: 当只想下载文件中的某1片段时指定
	 */
	public download(fileId: string, options: { target?: any; offset?: number; bytes?: number }): Promise<any> {
		return this._getConnection().then((socket) => {
			const gf = helpers.id2gf(fileId);
			const charset = this.config.charset;
			// 封装header
			const fnLength = Buffer.byteLength(gf.filename, charset);
			const bodyLength = protocol.FDFS_PROTO_PKG_LEN_SIZE + protocol.FDFS_PROTO_PKG_LEN_SIZE + protocol.FDFS_GROUP_NAME_MAX_LEN + fnLength;
			const header = protocol.packHeader(protocol.STORAGE_PROTO_CMD_DOWNLOAD_FILE, bodyLength, 0);

			// 封装body
			const body: any = Buffer.alloc(bodyLength);
			// 默认都填充上0
			body.fill(0);
			if (options.offset) {
				helpers.number2Buffer(options.offset, protocol.FDFS_PROTO_PKG_LEN_SIZE).copy(body);
			}
			if (options.bytes) {
				helpers.number2Buffer(options.bytes, protocol.FDFS_PROTO_PKG_LEN_SIZE).copy(body, protocol.FDFS_PROTO_PKG_LEN_SIZE);
			}
			const groupBL = Buffer.byteLength(gf.group, charset);
			body.write(gf.group, protocol.FDFS_PROTO_PKG_LEN_SIZE + protocol.FDFS_PROTO_PKG_LEN_SIZE, groupBL, charset);
			body.write(gf.filename, protocol.FDFS_PROTO_PKG_LEN_SIZE + protocol.FDFS_PROTO_PKG_LEN_SIZE + protocol.FDFS_GROUP_NAME_MAX_LEN, fnLength, charset);

			socket.write(Buffer.concat([header, body]) as any);

			return new Promise<any>((resolve, reject) => {
				let header: any;
				const target = options.target;
				// 已接收的body length
				let recvLength = 0;

				protocol.recvPacket(socket, protocol.STORAGE_PROTO_CMD_RESP, null, (err, data) => {
					if (err) {
						reject(err);
						return;
					}
					// header
					if (!is.buffer(data)) {
						header = data;

						// body
					} else {
						if (!target) {
							resolve({
								data: data,
								bodyLength: header.bodyLength
							});
						} else {
							target.write(data);
						}

						recvLength += data.length;
						// 读取完毕
						if (recvLength >= header.bodyLength) {
							// 读取完毕要关闭文件。
							// 不关闭文件造成两种后果：1、文件句柄不释放，造成硬盘空间不释放。
							// 2、在文件还没有写完时候就返回文件,表现如express中返回图片只有一半。
							target.on('close', function () {
								protocol.closeSocket(socket);
								resolve(null);
							});
							target.on('finish', function () {
								protocol.closeSocket(socket);
								resolve(null);
							});
							target.end();
						}
					}
				}, true);
			});
		});
	}

	/**
	 * * STORAGE_PROTO_CMD_QUERY_FILE_INFO
	 * # function: query file info from storage server
	 * # request body:
	 * @ FDFS_GROUP_NAME_MAX_LEN bytes: group name
	 * @ filename bytes: filename
	 * # response body:
	 * @ FDFS_PROTO_PKG_LEN_SIZE bytes: file size
	 * @ FDFS_PROTO_PKG_LEN_SIZE bytes: file create timestamp
	 * @ FDFS_PROTO_PKG_LEN_SIZE bytes: file CRC32 signature
	 * @ FDFS_IPADDR_SIZE bytes: file source ip addr
	 * @param fileId
	 */
	public getFileInfo(fileId: string): Promise<any> {
		const charset = this.config.charset;
		return this._getConnection().then((socket) => {
			const gf = helpers.id2gf(fileId);
			const packet = protocol.packFileId(protocol.STORAGE_PROTO_CMD_QUERY_FILE_INFO, gf.group, gf.filename, charset);
			socket.write(packet);

			return new Promise<any>((resolve, reject) => {
				protocol.recvPacket(socket, protocol.STORAGE_PROTO_CMD_RESP, protocol.FDFS_PROTO_PKG_LEN_SIZE * 3 + protocol.FDFS_IPADDR_SIZE, (err, body) => {
					if (err) {
						reject(err);
						return;
					}
					const result = {
						size: parseInt(body.toString('hex', 0, protocol.FDFS_PROTO_PKG_LEN_SIZE), 16),
						timestamp: helpers.buffer2Number(body, protocol.FDFS_PROTO_PKG_LEN_SIZE),
						crc32: helpers.buffer2Number(body, protocol.FDFS_PROTO_PKG_LEN_SIZE * 2),
						addr: helpers.trim(body.toString(charset, protocol.FDFS_PROTO_PKG_LEN_SIZE * 3))
					};
					resolve(result);
				});
			});
		});
	}

}

// helpers
/**
 * parse file id from body
 * @param body
 * @param charset
 */
function _parseFileId(body, charset) {
    var group = helpers.trim(body.toString(charset, 0, protocol.FDFS_GROUP_NAME_MAX_LEN));
    var filename = helpers.trim(body.toString(charset, protocol.FDFS_GROUP_NAME_MAX_LEN));
    return group + '/' + filename;
}
